import event from '../utils/event'
import { Token } from './model'
import { TokenType } from './constant'
import { isNotNull, isNotEmpty, isNull } from '../utils'


class MonkeyLexer {

    constructor(sourceCode) {
        this.sourceCode = sourceCode
        this.readPosition = 0
        this.lineCount = 0
        this.initKeywords()
    }

    initKeywords() {
        this.keywordMap = {
            "if": TokenType.IF,
            "else": TokenType.ELSE,
            "let": TokenType.LET,
            "fn": TokenType.FUNCTION,
            "true": TokenType.TRUE,
            "false": TokenType.FALSE,
            "return": TokenType.RETURN
        }
    }

    readChar() {
        const ch = this.peekChar()
        this.readPosition++
        return ch
    }

    peekChar(tick = 0) {
        if (this.readPosition + tick >= this.sourceCode?.length) {
            return -1
        } else {
            return this.sourceCode[this.readPosition + tick]
        }
    }

    skipChar() {
        this.readPosition++
        return this.peekChar()
    }


    /**
     * 忽略空白符
     */
    skipWhiteSpaceAndNewLine() {
        let ch = this.peekChar()
        while (ch === ' ' || ch === '\t' || ch === '\u00a0' || ch === '\n') {
            if (ch === '\t' || ch === '\n') {
                this.lineCount++
            }
            ch = this.skipChar()
        }
    }

    nextToken() {
        let tok
        this.skipWhiteSpaceAndNewLine()
        const lineCount = this.lineCount
        const ch = this.peekChar()
        switch(ch) {
            case '"':
                let str = this.readString()
                if (isNull(str)) {
                    tok = new Token(TokenType.ILLEGAL, null, lineCount, this.readPosition, this.readPosition)
                } else {
                    tok = new Token(TokenType.STRING, str, lineCount, this.readPosition - str.length, this.readPosition)
                }
                break
            case '=':
                if (this.peekChar(1) === "=") {
                    tok = new Token(TokenType.EQ, `${this.readChar()}${this.readChar()}`, lineCount, this.readPosition - 2, this.readPosition)
                } else {
                    tok = new Token(TokenType.ASSIGN_SIGN, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                }
                break
            case ';':
                tok = new Token(TokenType.SEMICOLON, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '+':
                tok = new Token(TokenType.PLUS_SIGN, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case -1:
                tok = new Token(TokenType.EOF, "", lineCount, this.readPosition, this.readPosition)
                break
            case '-':
                tok = new Token(TokenType.MINUS_SIGN, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '!':
                if (this.peekChar(1) === "=") {
                    tok = new Token(TokenType.NOT_EQ, `${this.readChar()}${this.readChar()}`, lineCount, this.readPosition - 2, this.readPosition)
                } else {
                    tok = new Token(TokenType.BANG_SIGN, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                }
                break
            case '*':
                tok = new Token(TokenType.ASTERISK, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break;
            case '/':
                tok = new Token(TokenType.SLASH, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '<':
                tok = new Token(TokenType.LT, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '>':
                tok = new Token(TokenType.GT, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '[':
                tok = new Token(TokenType.LEFT_BRACKET, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case ']':
                tok = new Token(TokenType.RIGHT_BRACKET, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case ',':
                tok = new Token(TokenType.COMMA, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '{':
                tok = new Token(TokenType.LEFT_BRACE, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '}':
                tok = new Token(TokenType.RIGHT_BRACE, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case ':':
                tok = new Token(TokenType.COLON, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case '(':
                tok = new Token(TokenType.LEFT_PARENT, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            case ')':
                tok = new Token(TokenType.RIGHT_PARENT, this.readChar(), lineCount, this.readPosition - 1, this.readPosition)
                break
            default:
                let res = this.readIdentifier()
                if (res !== false) {
                    let keyword = this.keywordMap[res];
                    if (isNotNull(keyword)) {
                        tok = new Token(keyword, res, lineCount, this.readPosition - res.length, this.readPosition)
                    } else {
                        tok = new Token(TokenType.IDENTIFIER, res, lineCount, this.readPosition - res.length, this.readPosition)
                    }
                } else {
                    res = this.readNumber()
                    if (res !== false) {
                        tok = new Token(TokenType.INTEGER, res, lineCount, this.readPosition - res.length, this.readPosition)
                    }
                }
            if (res === false) {
                tok = null
            }
        }
        return tok
    }

    readString() {
        //  越过引号
        this.skipChar()
        let ch = this.readChar()
        let str = ""
        while(ch !== '"' && ch !== TokenType.EOF) {
            str += ch
            ch = this.readChar()
        }
        if (ch !== '"') {
            return null
        }
        return str
    }

    readIdentifier() {
        let identifier = ""
        while((this.isLetter(this.peekChar()))) {
            identifier += this.readChar()
        }
        if (identifier.length > 0) {
            return identifier
        } else {
            return false
        }
    }

    readNumber() {
        let number = ""
        while(this.isDigit(this.peekChar())) {
            number += this.readChar()
        }
        if (number.length > 0) {
            return number
        } else {
            return false
        }
    }

    isLetter(ch) {
        return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch === '_')
    }

    isDigit(ch) {
        return '0' <= ch && ch <= '9'
    }

    isReady() {
        return isNotEmpty(this.sourceCode)
    }

    lexing() {
        const tokens = []
        let token = this.nextToken()
        while(isNotNull(token) && token.type !== TokenType.EOF) {
            tokens.push(token)
            token = this.nextToken()
        }
        tokens.push(token)
        this.tokens = tokens
        const keywordValues = Object.values(this.keywordMap)
        const keywordTokens = [...tokens].filter(t => keywordValues.includes(t.type))
        if (keywordTokens.length) {
            event.dispatchEvent('keyword', keywordTokens)
        }
        return tokens
    }
}

export default MonkeyLexer